<template>
    <div
            class="el-tree"
            :class="{
      'el-tree--highlight-current': highlightCurrent,
      'is-dragging': !!dragState.draggingNode,
      'is-drop-not-allow': !dragState.allowDrop,
      'is-drop-inner': dragState.dropType === 'inner'
    }"
            role="tree"
    >
        <el-tree-node
                v-for="child in root.childNodes"
                :node="child"
                :props="props"
                :render-after-expand="renderAfterExpand"
                :show-checkbox="showCheckbox"
                :key="getNodeKey(child)"
                :render-content="renderContent"
                @node-expand="handleNodeExpand">
        </el-tree-node>
        <div class="el-tree__empty-block" v-if="isEmpty">
            <span class="el-tree__empty-text">{{ emptyText }}</span>
        </div>
        <div
                v-show="dragState.showDropIndicator"
                class="el-tree__drop-indicator"
                ref="dropIndicator">
        </div>
    </div>
</template>

<script>
    import TreeStore from './model/tree-store';
    import {getNodeKey, findNearestComponent} from './model/util';
    import ElTreeNode from './tree-node.vue';
    import {t} from 'element-ui/src/locale';
    import emitter from 'element-ui/src/mixins/emitter';
    import {addClass, removeClass} from 'element-ui/src/utils/dom';

    export default {
        name: 'ElTree',

        mixins: [emitter],

        components: {
            ElTreeNode
        },

        data() {
            return {
                store: null,
                root: null,
                currentNode: null,
                treeItems: null,
                checkboxItems: [],
                dragState: {
                    showDropIndicator: false,
                    draggingNode: null,
                    dropNode: null,
                    allowDrop: true
                }
            };
        },

        props: {
            data: {
                type: Array
            },
            emptyText: {
                type: String,
                default() {
                    return t('el.tree.emptyText');
                }
            },
            renderAfterExpand: {
                type: Boolean,
                default: true
            },
            nodeKey: String,
            checkStrictly: Boolean,
            defaultExpandAll: Boolean,
            expandOnClickNode: {
                type: Boolean,
                default: true
            },
            checkOnClickNode: Boolean,
            checkDescendants: {
                type: Boolean,
                default: false
            },
            autoExpandParent: {
                type: Boolean,
                default: true
            },
            defaultCheckedKeys: Array,
            defaultExpandedKeys: Array,
            currentNodeKey: [String, Number],
            renderContent: Function,
            showCheckbox: {
                type: Boolean,
                default: false
            },
            draggable: {
                type: Boolean,
                default: false
            },
            allowDrag: Function,
            allowDrop: Function,
            props: {
                default() {
                    return {
                        children: 'children',
                        label: 'label',
                        disabled: 'disabled'
                    };
                }
            },
            lazy: {
                type: Boolean,
                default: false
            },
            highlightCurrent: Boolean,
            load: Function,
            filterNodeMethod: Function,
            accordion: Boolean,
            indent: {
                type: Number,
                default: 18
            },
            iconClass: String
        },

        computed: {
            children: {
                set(value) {
                    this.data = value;
                },
                get() {
                    return this.data;
                }
            },

            treeItemArray() {
                return Array.prototype.slice.call(this.treeItems);
            },

            isEmpty() {
                const {childNodes} = this.root;
                return !childNodes || childNodes.length === 0 || childNodes.every(({visible}) => !visible);
            }
        },

        watch: {
            defaultCheckedKeys(newVal) {
                this.store.setDefaultCheckedKey(newVal);
            },

            defaultExpandedKeys(newVal) {
                this.store.defaultExpandedKeys = newVal;
                this.store.setDefaultExpandedKeys(newVal);
            },

            data(newVal) {
                this.store.setData(newVal);
            },

            checkboxItems(val) {
                Array.prototype.forEach.call(val, (checkbox) => {
                    checkbox.setAttribute('tabindex', -1);
                });
            },

            checkStrictly(newVal) {
                this.store.checkStrictly = newVal;
            }
        },

        methods: {
            filter(value) {
                if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter');
                this.store.filter(value);
            },

            getNodeKey(node) {
                return getNodeKey(this.nodeKey, node.data);
            },

            getNodePath(data) {
                if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getNodePath');
                const node = this.store.getNode(data);
                if (!node) return [];
                const path = [node.data];
                let parent = node.parent;
                while (parent && parent !== this.root) {
                    path.push(parent.data);
                    parent = parent.parent;
                }
                return path.reverse();
            },

            getCheckedNodes(leafOnly, includeHalfChecked) {
                return this.store.getCheckedNodes(leafOnly, includeHalfChecked);
            },

            getCheckedKeys(leafOnly) {
                return this.store.getCheckedKeys(leafOnly);
            },

            getCurrentNode() {
                const currentNode = this.store.getCurrentNode();
                return currentNode ? currentNode.data : null;
            },

            getCurrentKey() {
                if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getCurrentKey');
                const currentNode = this.getCurrentNode();
                return currentNode ? currentNode[this.nodeKey] : null;
            },

            setCheckedNodes(nodes, leafOnly) {
                if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes');
                this.store.setCheckedNodes(nodes, leafOnly);
            },

            setCheckedKeys(keys, leafOnly) {
                if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys');
                this.store.setCheckedKeys(keys, leafOnly);
            },

            setChecked(data, checked, deep) {
                this.store.setChecked(data, checked, deep);
            },

            getHalfCheckedNodes() {
                return this.store.getHalfCheckedNodes();
            },

            getHalfCheckedKeys() {
                return this.store.getHalfCheckedKeys();
            },

            setCurrentNode(node) {
                if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode');
                this.store.setUserCurrentNode(node);
            },

            setCurrentKey(key) {
                if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey');
                this.store.setCurrentNodeKey(key);
            },

            getNode(data) {
                return this.store.getNode(data);
            },

            remove(data) {
                this.store.remove(data);
            },

            append(data, parentNode) {
                this.store.append(data, parentNode);
            },

            insertBefore(data, refNode) {
                this.store.insertBefore(data, refNode);
            },

            insertAfter(data, refNode) {
                this.store.insertAfter(data, refNode);
            },

            handleNodeExpand(nodeData, node, instance) {
                this.broadcast('ElTreeNode', 'tree-node-expand', node);
                this.$emit('node-expand', nodeData, node, instance);
            },

            updateKeyChildren(key, data) {
                if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild');
                this.store.updateChildren(key, data);
            },

            initTabIndex() {
                this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
                this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
                const checkedItem = this.$el.querySelectorAll('.is-checked[role=treeitem]');
                if (checkedItem.length) {
                    checkedItem[0].setAttribute('tabindex', 0);
                    return;
                }
                this.treeItems[0] && this.treeItems[0].setAttribute('tabindex', 0);
            },

            handleKeydown(ev) {
                const currentItem = ev.target;
                if (currentItem.className.indexOf('el-tree-node') === -1) return;
                const keyCode = ev.keyCode;
                this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
                const currentIndex = this.treeItemArray.indexOf(currentItem);
                let nextIndex;
                if ([38, 40].indexOf(keyCode) > -1) { // up、down
                    ev.preventDefault();
                    if (keyCode === 38) { // up
                        nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0;
                    } else {
                        nextIndex = (currentIndex < this.treeItemArray.length - 1) ? currentIndex + 1 : 0;
                    }
                    this.treeItemArray[nextIndex].focus(); // 选中
                }
                if ([37, 39].indexOf(keyCode) > -1) { // left、right 展开
                    ev.preventDefault();
                    currentItem.click(); // 选中
                }
                const hasInput = currentItem.querySelector('[type="checkbox"]');
                if ([13, 32].indexOf(keyCode) > -1 && hasInput) { // space enter选中checkbox
                    ev.preventDefault();
                    hasInput.click();
                }
            }
        },

        created() {
            this.isTree = true;

            this.store = new TreeStore({
                key: this.nodeKey,
                data: this.data,
                lazy: this.lazy,
                props: this.props,
                load: this.load,
                currentNodeKey: this.currentNodeKey,
                checkStrictly: this.checkStrictly,
                checkDescendants: this.checkDescendants,
                defaultCheckedKeys: this.defaultCheckedKeys,
                defaultExpandedKeys: this.defaultExpandedKeys,
                autoExpandParent: this.autoExpandParent,
                defaultExpandAll: this.defaultExpandAll,
                filterNodeMethod: this.filterNodeMethod
            });

            this.root = this.store.root;

            let dragState = this.dragState;
            this.$on('tree-node-drag-start', (event, treeNode) => {
                if (typeof this.allowDrag === 'function' && !this.allowDrag(treeNode.node)) {
                    event.preventDefault();
                    return false;
                }
                event.dataTransfer.effectAllowed = 'move';

                // wrap in try catch to address IE's error when first param is 'text/plain'
                try {
                    // setData is required for draggable to work in FireFox
                    // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
                    event.dataTransfer.setData('text/plain', '');
                } catch (e) {
                }
                dragState.draggingNode = treeNode;
                this.$emit('node-drag-start', treeNode.node, event);
            });

            this.$on('tree-node-drag-over', (event, treeNode) => {
                const dropNode = findNearestComponent(event.target, 'ElTreeNode');
                const oldDropNode = dragState.dropNode;
                if (oldDropNode && oldDropNode !== dropNode) {
                    removeClass(oldDropNode.$el, 'is-drop-inner');
                }
                const draggingNode = dragState.draggingNode;
                if (!draggingNode || !dropNode) return;

                let dropPrev = true;
                let dropInner = true;
                let dropNext = true;
                let userAllowDropInner = true;
                if (typeof this.allowDrop === 'function') {
                    dropPrev = this.allowDrop(draggingNode.node, dropNode.node, 'prev');
                    userAllowDropInner = dropInner = this.allowDrop(draggingNode.node, dropNode.node, 'inner');
                    dropNext = this.allowDrop(draggingNode.node, dropNode.node, 'next');
                }
                event.dataTransfer.dropEffect = dropInner ? 'move' : 'none';
                if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) {
                    if (oldDropNode) {
                        this.$emit('node-drag-leave', draggingNode.node, oldDropNode.node, event);
                    }
                    this.$emit('node-drag-enter', draggingNode.node, dropNode.node, event);
                }

                if (dropPrev || dropInner || dropNext) {
                    dragState.dropNode = dropNode;
                }

                if (dropNode.node.nextSibling === draggingNode.node) {
                    dropNext = false;
                }
                if (dropNode.node.previousSibling === draggingNode.node) {
                    dropPrev = false;
                }
                if (dropNode.node.contains(draggingNode.node, false)) {
                    dropInner = false;
                }
                if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) {
                    dropPrev = false;
                    dropInner = false;
                    dropNext = false;
                }

                const targetPosition = dropNode.$el.getBoundingClientRect();
                const treePosition = this.$el.getBoundingClientRect();

                let dropType;
                const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.45 : 1)) : -1;
                const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.55 : 0)) : 1;

                let indicatorTop = -9999;
                const distance = event.clientY - targetPosition.top;
                if (distance < targetPosition.height * prevPercent) {
                    dropType = 'before';
                } else if (distance > targetPosition.height * nextPercent) {
                    dropType = 'after';
                } else if (dropInner) {
                    dropType = 'inner';
                } else {
                    dropType = 'none';
                }

                const iconPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect();
                const dropIndicator = this.$refs.dropIndicator;
                if (dropType === 'before') {
                    indicatorTop = iconPosition.top - treePosition.top;
                } else if (dropType === 'after') {
                    indicatorTop = iconPosition.bottom - treePosition.top;
                }
                dropIndicator.style.top = indicatorTop + 'px';
                dropIndicator.style.left = (iconPosition.right - treePosition.left) + 'px';

                if (dropType === 'inner') {
                    addClass(dropNode.$el, 'is-drop-inner');
                } else {
                    removeClass(dropNode.$el, 'is-drop-inner');
                }

                dragState.showDropIndicator = dropType === 'before' || dropType === 'after';
                dragState.allowDrop = dragState.showDropIndicator || userAllowDropInner;
                dragState.dropType = dropType;
                this.$emit('node-drag-over', draggingNode.node, dropNode.node, event);
            });

            this.$on('tree-node-drag-end', (event) => {
                const {draggingNode, dropType, dropNode} = dragState;
                event.preventDefault();
                event.dataTransfer.dropEffect = 'move';

                if (draggingNode && dropNode) {
                    const draggingNodeCopy = {data: draggingNode.node.data};
                    if (dropType !== 'none') {
                        draggingNode.node.remove();
                    }
                    if (dropType === 'before') {
                        dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node);
                    } else if (dropType === 'after') {
                        dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node);
                    } else if (dropType === 'inner') {
                        dropNode.node.insertChild(draggingNodeCopy);
                    }
                    if (dropType !== 'none') {
                        this.store.registerNode(draggingNodeCopy);
                    }

                    removeClass(dropNode.$el, 'is-drop-inner');

                    this.$emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event);
                    if (dropType !== 'none') {
                        this.$emit('node-drop', draggingNode.node, dropNode.node, dropType, event);
                    }
                }
                if (draggingNode && !dropNode) {
                    this.$emit('node-drag-end', draggingNode.node, null, dropType, event);
                }

                dragState.showDropIndicator = false;
                dragState.draggingNode = null;
                dragState.dropNode = null;
                dragState.allowDrop = true;
            });
        },

        mounted() {
            this.initTabIndex();
            this.$el.addEventListener('keydown', this.handleKeydown);
        },

        updated() {
            this.treeItems = this.$el.querySelectorAll('[role=treeitem]');
            this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
        }
    };
</script>
